import os
import dep_graph.keys
import repo_path
import logging
import errno
import zipimport

from build_file.gen_rule_pausing import GenRuleFunc

logger = logging.getLogger(__name__)
class BuildSyntaxError(repo_path.Error):
  """Exception representing a syntax error in a build file."""

  def __init__(self, e):
    """Constructs a BuildSyntaxError.

    Args:
      e: SyntaxError, The syntax error to grab information from.
    """
    super().__init__("Syntax Error in Build File")
    self.e = e

  def __str__(self):
    """Returns a string representation of the exception"""
    e = self.e
    o = e.offset - 1
    if o < 0:
      o = 0
    return ("Syntax Error(" + e.msg + "): " + e.filename + ":" +
        str(e.lineno) + ":"  + e.text[0:o] + '\033[7m' +
        e.text[o:o + 1] + '\033[27m' + e.text[o + 1:-1])

class RuleError(repo_path.Error):
  """Error detected by the rule."""

class BuildFileMissingError(repo_path.Error):
  """No build file."""

class CallState(object):
  """Environment used to Exec build files.

  This object is used to contain all the helpers when execing BUILD files.
  """

  def __init__(self, config, graph, variant):
    """Constructs a CallState.

    Args:
      config: config.RepoConfig, The configuration information for the build.
      graph: build_file.graph.BuildRuleCachedGraph, The graph to build up.
      variant: string, The variant to build.
    """
    self._graph = graph
    self._config = config

    # A stack of strings that are directories who's build files are recursively
    # being expanded.
    self._file_stack = []
    # TODO(austin): How is this different from the _file_stack?
    self.file_stack_calling = []
    # dict of key -> GenRulePaused mappings.
    self.paused = {}

    # The set of BUILD files that were loaded off disk.
    self._loaded_files = set()
    # dict of sets strings, where dict maps directories to sets of the rules
    # in that directory.
    self._file_listing = {}

    self._variant = variant

    # The global environment when evaluating config and BUILD files.
    self.objs_dict = {}
    self.objs_dict["GenRule"] = self.GenRule
    self.objs_dict["GenRuleFunc"] = self.GenRuleFunc
    self.objs_dict["getRule"] = self.getRule
    self.objs_dict["expand_out_path"] = self.expand_out_path
    self.objs_dict["expand_out_path_for_rule"] = self.expand_out_path_for_rule
    self.objs_dict["output_basename"] = self.output_basename
    self.objs_dict["expand_path"] = self.expand_path
    self.objs_dict["RuleError"] = RuleError
    self.objs_dict["option"] = self.option
    self.objs_dict["variant"] = self.variant

  @property
  def loaded_files(self):
    """Returns the list of BUILD files that were loaded off disk.

    Returns:
      set(string), the BUILD file paths in the repository.
    """
    return self._loaded_files

  def GenRule(self, name):
    """Creates a GenRule and puts it in the graph.

    Args:
      name: string, The name of the rule.  Must be unique in the current
          directory.
    """
    key = dep_graph.keys.GenRuleKey(self.getCallingFile(), name, self._variant)
    rule = self._graph.create_gen_rule(key)
    logger.debug("created: " + str(rule))
    if key in self.paused:
      self.paused[key].set_output(rule)
    return rule

  def GenRuleFunc(self, *args, **kwargs):
    """Returns the GenRuleFunc generator."""
    return GenRuleFunc(self, *args, **kwargs)

  def getRule(self, name):
    """Returns the rule associated with a name.

    The name may be relative (:my_target), or absolute (//my:target)

    Returns:
      GenRule, the rule.
    """
    cwd = self.file_stack_calling[-1]
    return self._graph.get_rule_with_key(repo_path.pathToKey(name, self.variant, cwd))

  def get_gen_rule_with_key(self, key):
    """Returns the rule associated with the key.

    If the rule has not been created yet, but has been paused, run it.
    """
    self.ensureDirnameLoaded(key.dirname)
    # If the rule isn't in the paused list by now, it doesn't exist.
    if key not in self.paused:
      raise repo_path.Error("Error, could not find rule: " + key.full_name())
    # Continue running the rule if need be, and return the resulting rule.
    return self.paused[key].get()

  def ensureDirnameLoaded(self, dirname):
    """Loads rules from a directory if needed.

    Args:
      dirname: string, The directory name to load.
    """
    if dirname == '.':
      path_in_repo = 'BUILD'
    else:
      path_in_repo = os.path.join(dirname, 'BUILD')
    # Load the BUILD file if it isn't loaded already.
    if not path_in_repo in self._loaded_files:
      self.loadRuleFile(path_in_repo, dirname)

  def loadRuleFile(self, rule_file, dirname):
    """Loads rules from a BUILD file in a directory.

    Args:
      rule_file: string, the path in the repository to the file.
      dirname: string, the directory name.
    """
    logger.debug("loading: " + repr(rule_file))
    print('Loading', rule_file, 'for variant', self.variant)
    self._file_stack.append(dirname)
    self._loaded_files.add(rule_file)
    self._file_listing[dirname] = set()
    try:
      byte_code = compile(open(rule_file).read(), "//" + rule_file, "exec")
    except IOError:
      raise BuildFileMissingError("No such build file: " + repr("//" + rule_file))
    except SyntaxError as e:
      raise BuildSyntaxError(e)
    exec(byte_code, dict(self.objs_dict))
    self._file_stack.pop()

  def load_builtin_config_file(self, path, module):
    """Loads a config or genrule definition file into the call_state.

    Args:
      path_in_repo: string, The filename to load.
    """
    try:
      byte_code = compile(open(os.path.join(path, module + '.py')).read(), module, "exec")
    except IOError as err:
      if err.errno != errno.ENOTDIR:
        raise err
      else:
        z = zipimport.zipimporter(path)
        byte_code = z.get_code(module)

    exec(byte_code, self.objs_dict)

  def load_config_file(self, path_in_repo):
    """Loads a config or genrule definition file into the call_state.

    Args:
      path_in_repo: string, The filename to load.
    """
    byte_code = compile(open(path_in_repo).read(), path_in_repo, "exec")
    exec(byte_code, self.objs_dict)

  def getCallingFile(self):
    """Returns the directory in which the current rule is being evaluated.

    Returns:
      string, The directory.
    """
    return self.file_stack_calling[-1]

  def get_dashes(self):
    """Returns a number of dashes proportional to the stack depth."""
    return "-" * len(self.file_stack_calling)

  @property
  def file_stack(self):
    """Returns the current file stack."""
    return self._file_stack

  def announceRule(self, key, rule):
    self._graph.announceRule(key, rule);

  def announcePaused(self, dirname, name, paused):
    """Saves a paused rule.

    Args:
      dirname: string, the directory for the rule.
      name: string, the rule name.
      paused: GenRuleFunc, The paused rule.
    """
    key = dep_graph.keys.GenRuleKey(dirname, name, self._variant)
    self._file_listing[dirname].add(name)
    self.paused[key] = paused

  def expand_path(self, filename):
    """Expands the provided path to a repo absolute path.

    Args:
      filename: string, The filename to expand.  The path for the filename is
        relative to the current directory, and the output path is relative to
        the root of the repository.
    """
    folder_name = self.file_stack_calling[-1]
    if folder_name == '.':
      return filename
    return os.path.join(folder_name,filename)

  @property
  def variant(self):
    """Returns the variant that is currently being graphed."""
    return self._variant

  def option(self, option_name, default=None):
    """Returns an option value for the current variant, with a default.

    Args:
      option_name: string, The name of the option.
      default: The value to return if nothing matches.
    """
    return self._config.get_option(self.variant, option_name, default)

  def output_basename(self):
    """Returns the root directory for all build output."""
    return self._config.output_directory(self.variant)

  def expand_out_path_for_rule(self, rule, filename):
    """Expands a path relative to the rule's directory in the output folder.

    Args:
      rule: GenRule, the rule to expand a filename as.
      filename: string, the filename to expand.

    Returns:
      string, The expanded path.
    """
    if rule.dirname() == '.':
      folder_name = self.output_basename()
    else:
      folder_name = os.path.join(self.output_basename(), rule.dirname())
    path_in_repo = os.path.join(folder_name, filename)
    repo_path.ensure_path(os.path.dirname(path_in_repo))
    return path_in_repo

  def expand_out_path(self, filename):
    """Expands a path relative to the current directory in the output folder.

    Args:
      filename: string, the filename to expand.

    Returns:
      string, The expanded path.
    """
    folder_name = self.file_stack_calling[-1]
    if folder_name == ".":
      folder_name = self.output_basename()
    else:
      folder_name = os.path.join(self.output_basename(), folder_name)
    path_in_repo = os.path.join(folder_name, filename)
    repo_path.ensure_path(os.path.dirname(path_in_repo))
    return path_in_repo

  def get_rules_in_directory(self, dirname):
    """Returns the rules in a directory.

    Args:
      dirname: string, The directory to list.

    Returns:
      set(string), the rules in the directory
    """
    try:
      self.ensureDirnameLoaded(dirname)
      return self._file_listing[dirname]
    except BuildFileMissingError:
      return []
